/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { DependsNode } from 'arkanalyzer/lib/core/graph/DependsGraph';
import { ArkFile } from 'arkanalyzer/lib/core/model/ArkFile';
import { ImportInfo } from 'arkanalyzer/lib/core/model/ArkImport';
import type { File, FileDepsGraph, ImportInfo4Dep } from './fileComponent';
import { ExportType } from 'arkanalyzer/lib/core/model/ArkExport';
import {ArkAssignStmt, ArkPtrInvokeExpr, ModelUtils, Stmt} from 'arkanalyzer';
import {StringConstant} from 'arkanalyzer/lib/core/base/Constant';
import path from 'path';

export class ArkFileDeps {
    private static instance: ArkFileDeps;

    private constructor() {
    }

    public static getInstance(): ArkFileDeps {
        if (!this.instance) {
            this.instance = new ArkFileDeps();
        }
        return this.instance;
    }

    public addDeps(depsGraph: FileDepsGraph, node: DependsNode<File>, arkFile: ArkFile): void {
        let importInfos = arkFile.getImportInfos();
        if (importInfos && importInfos.length > 0) {
            for (let importInfo of importInfos) {
                this.processImportInfo(depsGraph, node, importInfo);
            }
        }
        this.processDynamicImportInfo(depsGraph, node, arkFile);
    }

    private processImportInfo(depsGraph: FileDepsGraph, src: DependsNode<File>, importInfo: ImportInfo): void {
        const nodeAttr = this.genNodeByImportInfo(importInfo);
        if (nodeAttr) {
            const dst = depsGraph.addDepsNode(nodeAttr.name, nodeAttr);
            const edge = depsGraph.addEdge(src, dst, { kind: 0, attr: new Map() });
            depsGraph.addImportInfo2Edge(edge.getEdgeAttr(), this.simplifyImportInfo(importInfo));
        }
    }

    private genNodeByImportInfo(importInfo: ImportInfo): File | undefined {
        const lazyExport = importInfo.getLazyExportInfo();
        if (lazyExport) {
            const exportArkFile = lazyExport.getDeclaringArkFile();
            if (exportArkFile) {
                return {
                    name: exportArkFile.getFilePath(),
                    kind: 0,
                };
            }
        }
        const from = importInfo.getFrom();
        if (!from) {
            return undefined;
        }
        else if (from.endsWith('.so')) {
            return {
                name: from,
                kind: 0,
            };
        }
        else {
            return {
                name: from,
                kind: 1,
            };
        }
    }

    private simplifyImportInfo(importInfo: ImportInfo): ImportInfo4Dep {
        return {
            importClauseName: importInfo.getImportClauseName(),
            importType: importInfo.getImportType(),
            importFrom: importInfo.getFrom(),
            nameBeforeAs: importInfo.getNameBeforeAs(),
            isDefault: importInfo.isDefault(),
            exportType: importInfo.getLazyExportInfo()?.getArkExport()?.getExportType().toString() || ExportType.UNKNOWN.toString(),
        };
    }

    private processDynamicImportInfo(depsGraph: FileDepsGraph, node: DependsNode<File>, arkFile: ArkFile): void {
        // get add methods under this ArkFile
        const methods = ModelUtils.getAllMethodsInFile(arkFile);

        // traverse stmts
        methods.forEach((method) => {
            method.getCfg()?.getStmts()?.forEach((stmt) => {
                this.processImportStmt(depsGraph, node, stmt);
            });
        });
    }

    private processImportStmt(depsGraph: FileDepsGraph, node: DependsNode<File>, stmt: Stmt): void {
        if (!(stmt instanceof ArkAssignStmt)) {
            return;
        }
        const rightOp = stmt.getRightOp();
        if (!(rightOp instanceof ArkPtrInvokeExpr)) {
            return;
        }
        if (rightOp.getMethodSignature().getMethodSubSignature().getMethodName() === 'import') {
            const args = rightOp.getArgs()[0];
            if (args instanceof StringConstant) {
                this.addNodeByDynamicImport(depsGraph, node,
                    path.join(stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath(), args.getValue()),
                    this.getDynamicImportName(stmt));
            }
        }
    }

    private addNodeByDynamicImport(depsGraph: FileDepsGraph, src: DependsNode<File>, from: string, importName: string): void {
        if (from === '') {
            return;
        }
        const nodeAttr = {
            name: from,
            kind: from.endsWith('.so') ? 0 : 1
        };
        const dst = depsGraph.addDepsNode(nodeAttr.name, nodeAttr);
        const edge = depsGraph.addEdge(src, dst, { kind: 0, attr: new Map() });
        const importInfo: ImportInfo4Dep = {
            importClauseName: importName,
            importType: 'DynamicImport',
            importFrom: from
        };
        depsGraph.addImportInfo2Edge(edge.getEdgeAttr(), importInfo);
    }

    private getDynamicImportName(stmt: ArkAssignStmt): string {
        return '%DynamicImport%';
    }
}